前言
Webpack 是一个打包工具,实际开发中我们可能采用模块化、组件化进行开发,这些单个的文件(模块,Module)通过 rule 的配置使用不同的 Loader 进行处理,并从入口文件经过依赖解析最终输出一个或多个打包文件(模块代码的集合)。前面已经介绍了 Entry 入口文件的处理,本篇将对 Module 的处理部分进行源码分析。
1、前情提要
在第一篇中我们介绍了Compiler 到 Compilation 执行的过程,通过 entry 入口文件再到 Compilation.buildModule 进行模块处理,buildModule 才真正到了模块的部分。
|
|
在正式进入模块的内容之前,我们先对 NormalModuleFactory 进行介绍。在第一篇中我们在看到它被创建的一个过程,webpack 通过 NormalModuleFactory 来创建各类模块。同时,它也是 Tapable 的子类。因此,该类也有很多的 Hooks 供开发者使用。本篇不会对其做过多的源码解读,主要介绍下关键部分(这里我们会配合第一篇中 compilation 的 _addModuleChain 代码进行介绍):
|
|
Compilation 的 _addModuleChain 会调用 NormalModuleFactory 的 create 方法拿到一个 NormalModule 模块实例,中间主要是通过 NormalModuleFactory.hooks.resolver 拿到 loaders 等信息返回给 NormalModuleFactory.hooks.factory,然后去创建一个模块实例并通过回调返回。loaders 是在 NormalModuleFactory 的构造方法中通过 RuleSet 解析获得的。ok,那么通过这个过程,一个真正的模块(Module)实例就得到了,而且模块中包含它的 request 路径、loaders 处理器、context上下文(其实就是引用该模块的模块所在目录)、依赖模块等信息。这个模块可以通过路径信息进去读取,并被 loaders 进行处理。
2、Module
在前面的代码中我们已经看到,Compilation 的 buildModule 方法最红是调用了 Module 的 build方法。Module 类是一个基类,它的 build 方法并未实现,这个方法是在子类中被实现的。在上面对 NormalModuleFactory 的介绍中,我们看到它生成了一个 NormalModule 实例,我们将对这个类的 build 方法进行介绍,进入 build 方法的代码:
|
|
从源码上,build 方法是执行了 doBuild 方法。doBuild 最终走到了 runLoaders,这个方法由 loader-runner 库提供,这是一个独立的库。它可以用来开发调试你的自定义 Loader,我们在接下来的小节中去介绍。在这里 NormalModule 并没有过多的处理工作,基本就是交给 loaders 去处理模块内容。loaders 处理完之后使用了 parser 对象进行处理,最后是 _initBuildHash 去创建 hash。
3、loader-runner 库
笔者使用的 loader-runner 库版本如下:
现在,我们来看 runLoaders 方法的源码到底做了哪些事情:
|
|
我们看 runLoaders 方法对于传入的 options 进行了拆解和重新组装并调用了 iteratePitchingLoaders 方法。在 Webpack 中执行 loader 的过程分为两个阶段:pitch 和 evaluating。这里可以理解为事件里的 capturing 阶段,还没到执行。在 pitching 过程中你可以拦截执行。现在,我们来看 iteratePitchingLoaders 相关部分代码:
|
|
上面的源码中出现了几个方法:
- iteratePitchingLoaders,按照配置从头找到尾,然后从最后的 loader 开始执行
- iterateNormalLoaders,就是 iteratePitchingLoaders 的逆过程,依次执行 loader 的过程。这也可以解释为什么执行顺序和配置顺序是相反的。并且该方法里会判断退出时机
- processResource,决定 loader 的输入是读取模块内容还是上一个 loader 的输出
- runSyncOrAsync,真正调用 loader 函数的地方,并且给 loader 函数的 this 挂载 async 方法实现异步
4、再续前缘
ok,分析到这里。我们看到 webpack 是如何从 entry 到 module,最后执行 loader 处理 module 的过程。经过 loader 处理之后,最终一路 callback 返回的过程直到我们 addEntry 这里:
|
|
addEntry 最后执行了 callback,最终回调到了哪里? 还记得我们之前讲到的下面这段代码吗?
|
|
至此,结合前面 Tapable 的内容,这里最终 callback 回到了 Compilation 的 finish。
|
|
我们来看一下 Compilation 的 finish :
|
|
那么 callback 之后来到了 seal 方法:
|
|
seal 方法这段源码非常多,篇幅有限,我们并不会展开来讲。但是我们看到大部分 hooks 和方法的关键词 optimize、chunk 还有 hash。在执行 seal 方法之前,这里的 module 都是经过 loader 处理过的代码,还需要进行优化操作,比如压缩。最终在 this.hooks.afterSeal.callAsync 执行后回到 compiler 的 hooks.shouldEmit.call 以及 hooks.done.callAsync。到 done 这个 hooks(done 是只会执行一次的),基本上就是完成了打包文件的输出工作了,这也意味着一次完整的打包流程就完成了。
PS:seal 里面有很多的 hooks 执行了 call 或 callAsync 方法。我们在第一章中提到过 node_modules\webpack\lib\WebpackOptionsApply.js 这个类里用到了很多的插件,比如 SplitChunksPlugin、FlagDependencyUsagePlugin,它们添加的钩子都在这里执行。关于插件,我们在下一章去分析。